/**
 * 程序功能：使用MQTT协议，与阿里云物联网平台进行数据通信。
 *          在本示例程序中，实现了一次上报单个属性数据到云平台。
 * 使用教程：https://blog.yyzt.site/
 * 注意事项：1.所连接的WIFI名称和WIFI需要设置正确，并且仅支持连接2.4GHz的WIFI，不支持5GHz的WIFI
 *          2.阿里云物联网平台的设备证书（也叫三元组，ProductKey、DeviceName、DeviceSecret）需要设置正确
 *          3.相关属性名称标识符和数据类型需要设置正确
 * 参考资料：https://github.com/0xYootou/arduino-aliyun-iot-sdk
 *          https://github.com/legenddcr/aliyun-mqtt-arduino
 *          https://help.aliyun.com/document_detail/89301.html
 * 阿里云物联网平台网址：https://iot.console.aliyun.com/lk/summary/new
 */

#include <Arduino.h>          // 导入Arduino库文件
#include <YYZT_AliyunMqtt.h>  // 导入YYZT_AliyunMqtt库文件

/**
 * 串口调试打印相关参数
 */
#define debugState true             // 串口调试打印状态，true--开启调试打印，false--关闭调试打印
#define debugSerial Serial          // 调试打印的串口
#define debugSerialBuadrate 115200  // 调试打印的串口波特率

/**
 * 阿里云物联网平台相关参数
 */
#define productKey "a17RF1Mnqss"                         // 产品密钥 ProductKey
#define deviceName "ESP8266_001"                         // 设备名称 DeviceName
#define deviceSecret "7bb4991a4beabf6a14918a76819cd3bc"  // 设备密钥 DeviceSecret

#define INT_TEST_ID "INT_TEST"        // 整数型测试标识符
#define BOOL_TEST_ID "BOOL_TEST"      // 布尔型测试标识符
#define FLOAT_TEST_ID "FLOAT_TEST"    // 单精度浮点型测试标识符
#define STRING_TEST_ID "STRING_TEST"  // 字符串测试标识符

/**
 * WIFI相关参数
 */
#define wifiName "YYZT_2.4G"       // WIFI名称
#define wifiPassword "1234567890"  // WIFI密码

/**
 * MQTT相关参数
 */
#define mqttHeartbeatTime 60        // 心跳间隔时间，默认为60s，单位为：秒
#define mqttPacketSize 1024         // MQTT数据包的大小设置最大为1024字节
#define ckeckMqttConnectTime 10000  // 检查MQTT连接的间隔时间，默认为10秒
AliyunMqtt aliyunMqtt;              // 实例化aliyunMqtt对象

// MQTT的连接状态
typedef enum {
  MqttDisconnected = 0,  // MQTT已断开连接
  MqttConnecting,        // MQTT连接中
  MqttConnected          // MQTT已连接
} MqttConnectState;
uint8_t nowMqttConnectState = MqttDisconnected;  // 当前MQTT的连接状态，初始化为MqttDisconnected

/**
 * 函数功能：上报整数型测试属性，5秒上报1次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void reportINT_TEST_ID() {
  static uint32_t nowTime = millis();
  if (millis() - nowTime > 5000 && nowMqttConnectState == MqttConnected) {  // 如果大于发送时间并且MQTT已连接
    nowTime = millis();                                                     // 更新发送时间
    int data = random(-10000, 10000);                                       // 获取随机数值
    uint16_t result = aliyunMqtt.sendAttributeData(INT_TEST_ID, data);      // 发送数据到云平台
#if debugState
    debugSerial.println("上报整数型测试属性，发送数据为：[" + String(data) + "]，发送状态为：" + String(result ? "已发送，ID为：" + String(result) : "失败！"));
#endif
  }
}

/**
 * 函数功能：上报布尔型测试属性，10秒上报1次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void reportBOOL_TEST_ID() {
  static uint32_t nowTime = millis();
  if (millis() - nowTime > 10000 && nowMqttConnectState == MqttConnected) {  // 如果大于发送时间并且MQTT已连接
    nowTime = millis();                                                      // 更新发送时间
    bool data = random(0, 2);                                                // 获取随机数值
    uint16_t result = aliyunMqtt.sendAttributeData(BOOL_TEST_ID, data);      // 发送数据到云平台
#if debugState
    debugSerial.println("上报布尔型测试属性，发送数据为：[" + String(data) + "]，发送结果为：" + String(result ? "已发送，ID为：" + String(result) : "失败！"));
#endif
  }
}

/**
 * 函数功能：上报单精度浮点型测试属性，15秒上报1次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void reportFLOAT_TEST_ID() {
  static uint32_t nowTime = millis();
  if (millis() - nowTime > 15000 && nowMqttConnectState == MqttConnected) {  // 如果大于发送时间并且MQTT已连接
    nowTime = millis();                                                      // 更新发送时间
    float floatArray[5] = {1.23, 2.34, 3.45, 4.56, 5.67};                    // 定义一个float数组
    float data = floatArray[random(0, 5)];                                   // 获取随机数值
    uint16_t result = aliyunMqtt.sendAttributeData(FLOAT_TEST_ID, data);     // 发送数据到云平台
#if debugState
    debugSerial.println("上报单精度浮点型测试属性，发送数据为：[" + String(data) + "]，发送结果为：" + String(result ? "已发送，ID为：" + String(result) : "失败！"));
#endif
  }
}

/**
 * 函数功能：上报字符串测试属性，20秒上报1次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void reportSTRING_TEST_ID() {
  static uint32_t nowTime = millis();
  if (millis() - nowTime > 20000 && nowMqttConnectState == MqttConnected) {                                         // 如果大于发送时间并且MQTT已连接
    nowTime = millis();                                                                                             // 更新发送时间
    String stringArray[5] = {"123abc", "hello yyzt", "你好一叶遮天", "QQ:1436056045", "博客网址：blog.yyzt.site"};  // 定义一个String数组
    String data = stringArray[random(0, 5)];                                                                        // 获取随机数值
    uint16_t result = aliyunMqtt.sendAttributeData(STRING_TEST_ID, data);                                           // 发送数据到云平台
#if debugState
    debugSerial.println("字符串测试属性，发送数据为：[" + String(data) + "]，发送结果为：" + String(result ? "已发送，ID为：" + String(result) : "失败！"));
#endif
  }
}

/**
 * 函数功能：MQTT连接事件的回调函数
 * 参数1：[_sessionPresent] [bool] 会话是否存在
 * 返回值：无
 * 注意事项：无
 */
void onMqttConnect(bool _sessionPresent) {
#if debugState
  debugSerial.println("阿里云物联网MQTT服务器已连接，会话存在状态为：" + String(_sessionPresent));
#endif
  nowMqttConnectState = MqttConnected;  // 当前MQTT的连接状态为已连接
}

/**
 * 函数功能：MQTT断开连接事件的回调函数
 * 参数1：[_reason] [AsyncMqttClientDisconnectReason] MQTT断开连接的原因，在AsyncMqttClientDisconnectReason枚举中查看
 * 返回值：无
 * 注意事项：无
 */
void onMqttDisconnect(AsyncMqttClientDisconnectReason _reason) {
#if debugState
  debugSerial.println("阿里云物联网MQTT服务器已断开连接！原因为：" + String(uint8_t(_reason)));
#endif
  nowMqttConnectState = MqttDisconnected;  // 当前MQTT的连接状态为已断开连接
}

/**
 * 函数功能：检查MQTT和重连
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void checkMqttAndReconnect() {
  static uint8_t mqttConnectStateMachine = 0;  // MQTT连接的状态机
  static uint32_t nowTime = 0;                 // 检查时间
  switch (mqttConnectStateMachine) {
    // 检查MQTT的连接状态
    case 0: {
      if (MqttDisconnected == nowMqttConnectState) {  // 如果当前MQTT的连接状态为已断开连接
        aliyunMqtt.mqttClient.disconnect(true);       // 断开之前的连接
        aliyunMqtt.mqttClient.connect();              // 连接MQTT服务器，连接结果在连接事件的回调函数或断开连接事件的回调函数中处理
        mqttConnectStateMachine = 1;                  // MQTT连接中，状态机=1，重连中
        nowTime = millis();                           // 更新检查时间
#if debugState
        debugSerial.println("阿里云MQTT服务器已断开连接！开始尝试重连中......");
#endif
      }
    } break;
    // MQTT重连中
    case 1: {
      if (MqttConnected == nowMqttConnectState || millis() - nowTime > ckeckMqttConnectTime) {  // 如果当前MQTT的连接状态为已连接或者超过重连时间
        mqttConnectStateMachine = 0;                                                            // 状态机=0，回到检查中
      }
    } break;

    default:
      break;
  }
}

/**
 * 函数功能：程序初始化时调用，仅执行一次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void setup() {
  delay(1000);  // 等待设备上电稳定

#if debugState
  debugSerial.begin(debugSerialBuadrate);  // 初始化调试打印的串口
  debugSerial.println("\r\n程序开始初始化......");
#endif

  WiFi.disconnect(true);                   // 断开当前WIFI连接
  WiFi.mode(WIFI_STA);                     // 设置WIFI模式为STA模式
  WiFi.begin(wifiName, wifiPassword);      // 开始连接WIFI
  while (WL_CONNECTED != WiFi.status()) {  // 判断WIFI的连接状态
    delay(500);
#if debugState
    debugSerial.print(".");
#endif
  }

  aliyunMqtt.setDebug(debugSerial);                                       // 设置是否开启aliyunMqtt库中的打印，传入串口即开启打印
  aliyunMqtt.setDeviceCertificate(productKey, deviceName, deviceSecret);  // 设置连接阿里云物联网平台的设备证书（也叫三元组）

  aliyunMqtt.mqttClient.setKeepAlive(mqttHeartbeatTime);    // 设置心跳间隔时间
  aliyunMqtt.mqttClient.setMaxTopicLength(mqttPacketSize);  // 设置MQTT数据包大小
  aliyunMqtt.mqttClient.onConnect(onMqttConnect);           // 设置MQTT连接事件的回调函数
  aliyunMqtt.mqttClient.onDisconnect(onMqttDisconnect);     // 设置MQTT断开连接事件的回调函数
  aliyunMqtt.mqttClient.connect();                          // 连接MQTT服务器，连接结果在连接事件的回调函数或断开连接事件的回调函数中处理
  nowMqttConnectState = MqttConnecting;                     // 当前MQTT的连接状态为连接中

#if debugState
  debugSerial.println("\r\n程序初始化完成！");
#endif
}

/**
 * 函数功能：程序主循环函数，重复运行
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void loop() {
  checkMqttAndReconnect();  // 检查MQTT和重连
  reportINT_TEST_ID();      // 上报整数型测试属性，5秒上报1次
  reportBOOL_TEST_ID();     // 上报布尔型测试属性，10秒上报1次
  reportFLOAT_TEST_ID();    // 上报单精度浮点型测试属性，15秒上报1次
  reportSTRING_TEST_ID();   // 上报字符串测试属性，20秒上报1次
}
